{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Copyright 2018 Google LLC  \n",
    "  \n",
    " Licensed under the Apache License, Version 2.0 (the \"License\");  \n",
    " you may not use this file except in compliance with the License.  \n",
    " You may obtain a copy of the License at  \n",
    "  \n",
    "     http://www.apache.org/licenses/LICENSE-2.0  \n",
    "  \n",
    " Unless required by applicable law or agreed to in writing, software  \n",
    " distributed under the License is distributed on an \"AS IS\" BASIS,  \n",
    " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  \n",
    " See the License for the specific language governing permissions and  \n",
    " limitations under the License."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Only for when running on Colab:\n",
    "import sys\n",
    "if 'google.colab' in sys.modules:\n",
    "    # Get the dependency .py files, if any.\n",
    "    ! git clone https://github.com/GoogleCloudPlatform/cloudml-samples.git\n",
    "    ! cp cloudml-samples/tpu/templates/tpu_film_estimator/* .\n",
    "\n",
    "    # Authenticate the user for better GCS access.\n",
    "    # Copy verification code into the text field to continue.\n",
    "    from google.colab import auth\n",
    "    auth.authenticate_user()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import argparse\n",
    "import numpy as np\n",
    "import tensorflow as tf\n",
    "from tensorflow.contrib import summary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "N_CLASSES = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Making the filter sizes a global variable so it's eaiser to coordinate  \n",
    " between the modulation sub-network and the convolutional classifier  \n",
    " sub-network."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "FILTER_SIZES = [32, 64]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A linear modulation will be applied to every filter/feature map."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "N_FILM = sum(FILTER_SIZES)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Feature-wise Linear Modulation Layer  \n",
    "  \n",
    " The feature-wise linear modulation layer is a network architecture design  \n",
    " that allows contextual inputs to modulate classification layers.  \n",
    "   \n",
    " For details, see [FiLM: Visual Reasoning with a General Conditioning Layer](https://arxiv.org/abs/1709.07871)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class FeaturewiseLinearModulationLayer(tf.keras.layers.Layer):\n",
    "    def call(self, input_, gamma, beta):\n",
    "        # The user is responsible for having the correct shapes\n",
    "        return gamma * input_ + beta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The model function  \n",
    "   \n",
    " The network consists of two sub-networks:  \n",
    "  \n",
    " * Label classifier: A feedforward network (here convolutional).    \n",
    "   It is linearly modulated at intermediate outputs.  \n",
    "  \n",
    " * Modulation: A separate sub-network that learns the modulation parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def model_fn(features, labels, mode, params):\n",
    "    x = features['x']\n",
    "    modulation_data = features['modulation_data']\n",
    "    onehot_labels = tf.one_hot(labels, N_CLASSES)\n",
    "\n",
    "    batch_size = params.get('batch_size', None) or params['train_batch_size']\n",
    "\n",
    "    global_step = tf.train.get_global_step()\n",
    "\n",
    "    # In this sample we use dense layers for the modulation sub-network.\n",
    "    # Its output has shape (batch_size, 2 * N_FILM) since each FiLM layer has\n",
    "    # two parameters.\n",
    "    modulation_hidden = tf.keras.layers.Dense(128, activation=tf.nn.relu)(modulation_data)\n",
    "\n",
    "    # We want to allow negative modulation parameters. \n",
    "    # Here we just use the linear activation.\n",
    "    modulation_parameters = tf.keras.layers.Dense(2 * N_FILM)(modulation_hidden)\n",
    "\n",
    "    all_gamma = modulation_parameters[:, :N_FILM]\n",
    "    all_beta = modulation_parameters[:, N_FILM:]\n",
    "\n",
    "    # Convolutional layers for the label classifier.\n",
    "    filter_0 = FILTER_SIZES[0]\n",
    "    conv_0 = tf.keras.layers.Conv2D(filters=filter_0, kernel_size=(3, 3))(x)\n",
    "\n",
    "    # Apply FiLM before the ReLU activation.\n",
    "    # Reshape the modulation parameters manually.\n",
    "    gamma_0 = all_gamma[:, None, None, :filter_0]\n",
    "    beta_0 = all_beta[:, None, None, :filter_0]\n",
    "    filmed_conv_0 = FeaturewiseLinearModulationLayer()(conv_0, gamma_0, beta_0)\n",
    "\n",
    "    conv_out_0 = tf.nn.relu(filmed_conv_0)\n",
    "\n",
    "    # Do the same for the next convolutional block\n",
    "    filter_1 = FILTER_SIZES[1]\n",
    "    conv_1 = tf.keras.layers.Conv2D(filters=filter_1, kernel_size=(3, 3))(conv_out_0)\n",
    "\n",
    "    gamma_1 = all_gamma[:, None, None, -filter_1:]\n",
    "    beta_1 = all_beta[:, None, None, -filter_1:]\n",
    "    filmed_conv_1 = FeaturewiseLinearModulationLayer()(conv_1, gamma_1, beta_1)\n",
    "\n",
    "    conv_out_1 = tf.nn.relu(filmed_conv_1)\n",
    "\n",
    "    # Fully connected logits output\n",
    "    flattened = tf.reshape(conv_out_1, (batch_size, -1))\n",
    "    label_classification_logits = tf.keras.layers.Dense(N_CLASSES)(flattened)\n",
    "\n",
    "    predictions = tf.nn.softmax(label_classification_logits)\n",
    "    loss = None\n",
    "    train_op = None\n",
    "\n",
    "    if mode == tf.estimator.ModeKeys.TRAIN:\n",
    "        # define loss\n",
    "        loss = tf.losses.softmax_cross_entropy(\n",
    "            onehot_labels=onehot_labels,\n",
    "            logits=label_classification_logits\n",
    "        )\n",
    "\n",
    "        # define train_op\n",
    "        optimizer = tf.train.RMSPropOptimizer(learning_rate=0.05)\n",
    "\n",
    "        # wrapper to make the optimizer work with TPUs\n",
    "        if params['use_tpu']:\n",
    "            optimizer = tf.contrib.tpu.CrossShardOptimizer(optimizer)\n",
    "\n",
    "        train_op = optimizer.minimize(loss, global_step=global_step)\n",
    "\n",
    "    if params['use_tpu']:\n",
    "        # TPU version of EstimatorSpec\n",
    "        return tf.contrib.tpu.TPUEstimatorSpec(\n",
    "            mode=mode,\n",
    "            predictions=predictions,\n",
    "            loss=loss,\n",
    "            train_op=train_op)\n",
    "    else:\n",
    "        return tf.estimator.EstimatorSpec(\n",
    "            mode=mode,\n",
    "            predictions=predictions,\n",
    "            loss=loss,\n",
    "            train_op=train_op)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The input function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def train_input_fn(params={}):\n",
    "    # labaled image data\n",
    "    x = np.random.rand(100, 28, 28, 3)\n",
    "    y = np.random.randint(0, N_CLASSES, 100)\n",
    "\n",
    "    # additional input data for modulation\n",
    "    modulation_data = np.random.rand(100, 5)\n",
    "\n",
    "    x_tensor = tf.constant(x, dtype=tf.float32)\n",
    "    y_tensor = tf.constant(y, dtype=tf.int32)\n",
    "    modulation_data_tensor = tf.constant(modulation_data, dtype=tf.float32)\n",
    "\n",
    "    # make a dataset\n",
    "    dataset = tf.data.Dataset.from_tensor_slices((x_tensor, y_tensor, modulation_data_tensor))\n",
    "\n",
    "    # TPUEstimator passes params when calling input_fn\n",
    "    batch_size = params.get('batch_size', 16)\n",
    "\n",
    "    dataset = dataset.repeat().shuffle(32).batch(batch_size, drop_remainder=True)\n",
    "\n",
    "    # TPUs need to know all dimensions when the graph is built\n",
    "    # Datasets know the batch size only when the graph is run\n",
    "    def set_shapes_and_format(x, y, modulation_data):\n",
    "        \"\"\"Set the batch_size of the input tensors and returns a\n",
    "        pair (features, labels).\n",
    "        \"\"\"\n",
    "        x_shape = x.get_shape().merge_with([batch_size, None, None, None])\n",
    "        y_shape = y.get_shape().merge_with([batch_size])\n",
    "        modulation_data_shape = modulation_data.get_shape().merge_with([batch_size, None])\n",
    "\n",
    "        x.set_shape(x_shape)\n",
    "        y.set_shape(y_shape)\n",
    "        modulation_data.set_shape(modulation_data_shape)\n",
    "\n",
    "        # Also format the dataset with a dict for features\n",
    "        features = {'x': x, 'modulation_data': modulation_data}\n",
    "        labels = y\n",
    "\n",
    "        return features, labels\n",
    "\n",
    "    dataset = dataset.map(set_shapes_and_format)\n",
    "    dataset = dataset.prefetch(tf.contrib.data.AUTOTUNE)\n",
    "\n",
    "    return dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def main(args):\n",
    "    # pass the args as params so the model_fn can use\n",
    "    # the TPU specific args\n",
    "    params = vars(args)\n",
    "\n",
    "    if args.use_tpu:\n",
    "        # additional configs required for using TPUs\n",
    "        tpu_cluster_resolver = tf.contrib.cluster_resolver.TPUClusterResolver(args.tpu)\n",
    "        tpu_config = tf.contrib.tpu.TPUConfig(\n",
    "            num_shards=8, # using Cloud TPU v2-8\n",
    "            iterations_per_loop=args.save_checkpoints_steps)\n",
    "\n",
    "        # use the TPU version of RunConfig\n",
    "        config = tf.contrib.tpu.RunConfig(\n",
    "            cluster=tpu_cluster_resolver,\n",
    "            model_dir=args.model_dir,\n",
    "            tpu_config=tpu_config,\n",
    "            save_checkpoints_steps=args.save_checkpoints_steps,\n",
    "            save_summary_steps=100)\n",
    "\n",
    "        # TPUEstimator\n",
    "        estimator = tf.contrib.tpu.TPUEstimator(\n",
    "            model_fn=model_fn,\n",
    "            config=config,\n",
    "            params=params,\n",
    "            train_batch_size=args.train_batch_size,\n",
    "            eval_batch_size=32,\n",
    "            export_to_tpu=False)\n",
    "    else:\n",
    "        config = tf.estimator.RunConfig(model_dir=args.model_dir)\n",
    "\n",
    "        estimator = tf.estimator.Estimator(\n",
    "            model_fn,\n",
    "            config=config,\n",
    "            params=params)\n",
    "\n",
    "    estimator.train(train_input_fn, max_steps=args.max_steps)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "parser = argparse.ArgumentParser()\n",
    "\n",
    "parser.add_argument(\n",
    "    '--model-dir',\n",
    "    type=str,\n",
    "    default='/tmp/tpu-template',\n",
    "    help='Location to write checkpoints and summaries to.  Must be a GCS URI when using Cloud TPU.')\n",
    "parser.add_argument(\n",
    "    '--max-steps',\n",
    "    type=int,\n",
    "    default=1000,\n",
    "    help='The total number of steps to train the model.')\n",
    "parser.add_argument(\n",
    "    '--train-batch-size',\n",
    "    type=int,\n",
    "    default=16,\n",
    "    help='The training batch size.  The training batch is divided evenly across the TPU cores.')\n",
    "parser.add_argument(\n",
    "    '--save-checkpoints-steps',\n",
    "    type=int,\n",
    "    default=100,\n",
    "    help='The number of training steps before saving each checkpoint.')\n",
    "parser.add_argument(\n",
    "    '--use-tpu',\n",
    "    action='store_true',\n",
    "    help='Whether to use TPU.')\n",
    "parser.add_argument(\n",
    "    '--tpu',\n",
    "    default=None,\n",
    "    help='The name or GRPC URL of the TPU node.  Leave it as `None` when training on AI Platform.')\n",
    "\n",
    "args, _ = parser.parse_known_args()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# colab.research.google.com specific\n",
    "if 'google.colab' in sys.modules:\n",
    "    import json\n",
    "    import os\n",
    "\n",
    "    # TODO(user): change this\n",
    "    args.model_dir = 'gs://your-gcs-bucket'\n",
    "\n",
    "    # When connected to the TPU runtime\n",
    "    if 'COLAB_TPU_ADDR' in os.environ:\n",
    "        tpu_grpc = 'grpc://{}'.format(os.environ['COLAB_TPU_ADDR'])\n",
    "\n",
    "        args.tpu = tpu_grpc\n",
    "        args.use_tpu = True\n",
    "\n",
    "        # Upload credentials to the TPU\n",
    "        with tf.Session(tpu_grpc) as sess:\n",
    "            data = json.load(open('/content/adc.json'))\n",
    "            tf.contrib.cloud.configure_gcs(sess, credentials=data)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "main(args)"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 2
}
